---
title: Вращаем пространственный крест
description: Пишем алгоритм для поворота трёхмерной фигуры вокруг своего центра по всем трём осям сразу. В предыдущем примере мы вращали куб в пространстве — в этом...
sections: [Объёмные фигуры,Матрица поворота,Экспериментальная модель]
tags: [javascript,онлайн,canvas,геометрия,матрица,графика,изображение,картинка,квадрат,куб,3д,трёхмерный]
scripts: [/js/classes-point-cube.js,/js/spinning-spatial-cross.js,/js/spinning-spatial-cross2.js]
styles: [/css/pomodoro1.css]
canonical_url: /ru/2023/01/15/spinning-spatial-cross.html
url_translated: /en/2023/01/16/spinning-spatial-cross.html
title_translated: Spinning spatial cross
date: 2023.01.15
---

Пишем алгоритм для поворота трёхмерной фигуры вокруг своего центра по всем трём осям сразу. В предыдущем
примере мы [вращали куб в пространстве]({{ '/ru/2023/01/10/spinning-cube-in-space.html' | relative_url }})
— в этом примере кубиков будет много, алгоритм будет почти такой же, и формулы будем использовать те же. Для
наглядности возьмём два варианта симметричной объёмной фигуры в двух типах проекций — *пространственный крест*
и *крест-куб* — рассматриваем разницу между ними.

Тестирование экспериментального интерфейса: [Объёмный тетрис]({{ '/ru/2023/01/21/volumetric-tetris.html' | relative_url }}).

{% include heading.html text="Пространственный крест" hash="spatial-cross" %}

<div style="display: flex; flex-direction: row; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; padding-right: 8px;">
    <span>Параллельная проекция</span>
    <canvas id="canvas1" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
  <div style="display: flex; flex-direction: column;">
    <span>Перспективная проекция</span>
    <canvas id="canvas2" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
</div>

{% include heading.html text="Крест-куб" hash="cross-cube" %}

<div style="display: flex; flex-direction: row; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; padding-right: 8px;">
    <span>Параллельная проекция</span>
    <canvas id="canvas3" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
  <div style="display: flex; flex-direction: column;">
    <span>Перспективная проекция</span>
    <canvas id="canvas4" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
</div>

*Параллельная проекция* — все кубики одинакового размера.

*Перспективная проекция* — кубики выглядят уменьшающимися вдалеке.

{% include heading.html text="Экспериментальная модель" hash="experimental-model" %}

Слегка усложнённая версия из предыдущего примера — теперь кубиков много. В дополнение к предыдущим настройкам
можно поменять: вариант фигуры — *пространственный крест* или *крест-куб*, направление сортировки граней
— *прямая перспектива* или *обратная перспектива* и прозрачность стенок кубиков.

<div>
<canvas id="canvas5" width="300" height="300" style="border: 1px solid gray;">
  <p>Холст для отображения результатов вычислений</p>
</canvas>
</div>
<div>
<form>
  <div>
    <span>Вращение по осям:</span>
    <input type="checkbox" id="rotateX" name="rotateX" checked onchange="changeAxis('x',event)">
    <label for="rotateX">X</label>
    <input type="checkbox" id="rotateY" name="rotateY" checked onchange="changeAxis('y',event)">
    <label for="rotateY">Y</label>
    <input type="checkbox" id="rotateZ" name="rotateZ" checked onchange="changeAxis('z',event)">
    <label for="rotateZ">Z</label>
  </div>
  <span>Центр на экране наблюдателя:</span>
  <div>
    <input type="range" id="rangeX" name="rangeX" min="0" max="300" value="150" step="1"
           oninput="resultX.value = rangeX.valueAsNumber; changeTv('x',event)">
    <label for="rangeX">X</label>
    <output id="resultX" name="resultX">150</output>
  </div>
  <div>
    <input type="range" id="rangeY" name="rangeY" min="0" max="300" value="150" step="1"
           oninput="resultY.value = rangeY.valueAsNumber; changeTv('y',event)">
    <label for="rangeY">Y</label>
    <output id="resultY" name="resultY">150</output>
  </div>
  <div>
    <input type="range" id="rangeZ" name="rangeZ" min="50" max="220" value="125" step="1"
           oninput="resultZ.value = rangeZ.valueAsNumber; changeTv('z',event)">
    <label for="rangeZ">Z</label>
    <output name="resultZ">125</output>
  </div>
  <span>Удалённость центра проекции:</span>
  <div>
    <input type="range" id="distance" name="distance" min="220" max="800" value="300" step="10"
           oninput="result.value = distance.valueAsNumber; changeDistance(event);">
    <label for="distance">Z</label>
    <output name="result">300</output>
  </div>
  <div>
    <label for="center">Двигать центральную точку:</label>
    <input type="checkbox" id="center" name="center" oninput="showCenter(event)">
  </div>
  <span>Прозрачность кубиков:</span>
  <div>
    <input type="range" id="alpha" name="alpha" min="0" max="100" step="1" value="20"
           oninput="oAlpha.value = alpha.valueAsNumber + '%'; changeAlpha(event)">
    <label for="alpha">A</label>
    <output name="oAlpha">20%</output>
  </div>
</form>
<span>Вариант фигуры:</span>
<form oninput="changeFigure(event)">
  <input type="radio" id="first" name="figure" value="first">
  <label for="first">Крест</label>
  <input type="radio" id="second" name="figure" value="second" checked>
  <label for="second">Куб</label>
</form>
<span>Линейная перспектива:</span>
<form oninput="changeOrder(event)">
  <input type="radio" id="linear" name="order" value="linear" checked>
  <label for="linear">Прямая</label>
  <input type="radio" id="reverse" name="order" value="reverse">
  <label for="reverse">Обратная</label>
</form>
</div>

{% include heading.html text="Описание алгоритма" hash="algorithm-description" %}

Подготавливаем трёхмерную матрицу из нулей и единиц, где единица означает кубик в определенном месте фигуры.
Затем обходим эту матрицу и заполняем массив кубиков с соответствующими координатами вершин. После этого
запускаем вращение по всем трём осям сразу. На каждом шаге обходим массив кубиков и получаем проекции их
граней. Затем сортируем массив граней по удалённости от центра проекции, обходим этот массив и выкидываем
из него одинаковые пары — это есть смежные стенки между соседними кубиками внутри фигуры. После этого
рисуем полупрозрачным цветом грани кубиков — сначала дальние, а затем ближние, чтобы через ближние грани
было видно дальние.

{% include heading.html text="Реализация на JavaScript" hash="implementation-in-javascript" %}

{% include classes-point-cube-ru.md -%}

Создаём объекты по шаблонам и рисуем их проекции на плоскости.

```js
'use strict';
// матрицы-шаблоны для кубиков
const shape1 = [ // пространственный крест
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
  [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
const shape2 = [ // крест-куб
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
  [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
  [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
// размер кубика, количество кубиков в ряду, отступ
const size = 40, row = 5, gap = 50;
// массивы для кубиков
const cubes1 = [], cubes2 = [];
// обходим матрицы, заполняем массивы кубиками
for (let x=0; x<row; x++)
  for (let y=0; y<row; y++)
    for (let z=0; z<row; z++) {
      if (shape1[x][y][z]==1)
        cubes1.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
      if (shape2[x][y][z]==1)
        cubes2.push(new Cube(x*size+gap,y*size+gap,z*size+gap,size));
    }
// центр фигуры, вокруг него будем выполнять поворот
const t0 = new Point(150,150,150);
// удалённость центра проекции
const d = 300;
// положение экрана наблюдателя
const tv = new Point(150,150,125);
// угол поворота в градусах
const deg = {x:1,y:1,z:1};
```
```js
// рисовать будем по две картинки для каждой фигуры
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const canvas3 = document.getElementById('canvas3');
const canvas4 = document.getElementById('canvas4');
// обновление изображения
function repaint() {
  // пространственный крест
  processFigure(cubes1,canvas1,canvas2);
  // крест-куб
  processFigure(cubes2,canvas3,canvas4);
}
```
```js
// поворачиваем фигуру и получаем проекции
function processFigure(cubes,cnv1,cnv2) {
  // массивы проекций граней кубиков
  let parallel = [], perspective = [];
  // поворачиваем кубики и получаем проекции
  for (let cube of cubes) {
    cube.rotate(deg, t0);
    parallel = parallel.concat(cube.projection('parallel',tv,d));
    perspective = perspective.concat(cube.projection('perspective',tv,d));
  }
  // смежные стенки между соседними кубиками не рисуем
  noAdjacent(parallel);
  noAdjacent(perspective);
  // сортируем грани разных кубиков по удалённости и внутри одного кубика по наклону
  parallel.sort((a,b)=>Math.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
  // сортируем грани по удалённости от центра проекции
  perspective.sort((a,b)=>b.dist-a.dist);
  // рисуем параллельную проекцию
  drawFigure(cnv1, parallel);
  // рисуем перспективную проекцию
  drawFigure(cnv2, perspective);
}
```
```js
// смежные стенки между соседними кубиками не рисуем
function noAdjacent(array) {
  // сортируем грани по удалённости
  array.sort((a,b) => b.dist-a.dist);
  // удаляем смежные стенки между кубиками
  for (let i=0, j=1; i<array.length-1; j=++i+1)
    while (j<array.length && Cube.pEquidistant(array[i],array[j]))
      if (Cube.pAdjacent(array[i],array[j])) {
        array.splice(j,1);
        array.splice(i,1);
        i--; j=array.length;
      } else j++;
}
```
```js
// рисуем фигуру по точкам из массива
function drawFigure(canvas, proj, alpha=0.8) {
  const context = canvas.getContext('2d');
  // очищаем весь холст целиком
  context.clearRect(0, 0, canvas.width, canvas.height);
  // обходим массив граней куба
  for (let i = 0; i < proj.length; i++) {
    // обходим массив точек и соединяем их линиями
    context.beginPath();
    for (let j = 0; j < proj[i].length; j++) {
      if (j == 0) {
        context.moveTo(proj[i][j].x, proj[i][j].y);
      } else {
        context.lineTo(proj[i][j].x, proj[i][j].y);
      }
    }
    context.closePath();
    // рисуем грань куба вместе с рёбрами
    context.lineWidth = 1.9;
    context.lineJoin = 'round';
    context.fillStyle = 'rgba(200,230,201,'+alpha+')';
    context.strokeStyle = 'rgba(102,187,106,'+(0.2+alpha)+')';
    context.fill();
    context.stroke();
  }
}
```
```js
// после загрузки страницы, задаём частоту обновления изображения 20 Гц
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
```
